feat: #400 큐레이션 API 프론트 피드백 반영 - #412
Conversation
…om/LinkYou-2025/LinkU_backend into task/#400-curation-api-fe-feedback
- 외부 추천 링크를 사용자가 저장한 경우 처리 - 내부 추천 링크의 카테고리 null 오류 수정
Walkthrough큐레이션 추천 링크의 내부·외부 타입과 URL 정규화 방식을 정리하고, 외부 추천 필터링·사용자 링크 매칭·섹션 이미지 기반 썸네일·응답 변환·Swagger 문서를 변경했습니다. Changes큐레이션 링크 타입 및 추천 흐름
큐레이션 응답 API
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Code Coverage
|
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
src/main/java/com/umc/linkyou/service/curation/CurationServiceImpl.java (1)
90-94: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win섹션 1 이미지를 월별로 일괄 조회해 주세요.
존재하는 큐레이션마다
getSection1ImageUrl()이 별도 쿼리를 실행하므로, 목록 조회가 최대 1+12회 조회됩니다. 대상 월들의 section 1 정보를 한 번에 조회해Map<month, imageUrl>로 변환한 뒤 응답을 조립해 주세요.As per coding guidelines, “JPA fetch 전략을 적절히 설정하여 N+1 문제를 방지한다.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/java/com/umc/linkyou/service/curation/CurationServiceImpl.java` around lines 90 - 94, Update the monthly response assembly in CurationServiceImpl so section 1 image data for all target months is fetched in one query before the loop, converted to a Map keyed by month, and reused when building each CurationListResponse. Replace per-item getSection1ImageUrl(month) calls with map lookups while preserving null thumbnails for months without an existing curation.Source: Coding guidelines
src/test/java/com/umc/linkyou/service/curation/CurationServiceTest.java (1)
183-188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win섹션 1 썸네일 값 자체를 검증해 주세요.
현재 테스트는
"url"을 stub하지만 결과 개수만 확인합니다.2026-03항목의thumbnailUrl이"url"인지 검증해야 대표 이미지 변경 회귀를 잡을 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/test/java/com/umc/linkyou/service/curation/CurationServiceTest.java` around lines 183 - 188, CurationServiceTest의 getCurationList 검증에 2026-03 섹션 항목을 찾아 thumbnailUrl이 stub한 "url"과 일치하는지 검증을 추가하세요. 기존 결과 개수 검증은 유지하고, 섹션 1의 대표 이미지 값 자체를 확인하도록 수정하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.java`:
- Line 35: CurationRecommendBuilderServiceImpl 클래스에 `@Transactional`(readOnly =
true)를 적용하세요. ExternalRecommendWorker에도 클래스 레벨 기본 읽기 전용 트랜잭션을 추가하고, 저장 매핑을 수행하는
generateExternal 메서드에는 기존 메서드 레벨 `@Transactional을` 유지하세요. 대상 파일:
src/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.java
35-35,
src/main/java/com/umc/linkyou/service/curation/recommend/external/ExternalRecommendWorker.java
35-38.
In `@src/main/java/com/umc/linkyou/web/api/CurationApi.java`:
- Around line 52-57: CurationController#getLatestCuration의 빈 결과 계약과 Swagger 문서를
일치시키세요. 값이 없을 때의 204 No Content 응답을 `@ApiSuccessCode`(SuccessStatus._OK)와 함께 명시적으로
문서화하고, 생성된 큐레이션이 있는 경우의 200 응답 및 CurationLatestResponse 스키마는 유지하세요.
In
`@src/main/resources/db/migration/V19__drop_curation_linkus_url_normalized.sql`:
- Line 2: Update the migration before the DROP COLUMN statement to normalize all
existing curation_linkus.url values using the same normalization rules used by
the application, then remove url_normalized as currently planned. Ensure
existing external recommendation URLs are normalized in place before the column
is dropped.
---
Nitpick comments:
In `@src/main/java/com/umc/linkyou/service/curation/CurationServiceImpl.java`:
- Around line 90-94: Update the monthly response assembly in CurationServiceImpl
so section 1 image data for all target months is fetched in one query before the
loop, converted to a Map keyed by month, and reused when building each
CurationListResponse. Replace per-item getSection1ImageUrl(month) calls with map
lookups while preserving null thumbnails for months without an existing
curation.
In `@src/test/java/com/umc/linkyou/service/curation/CurationServiceTest.java`:
- Around line 183-188: CurationServiceTest의 getCurationList 검증에 2026-03 섹션 항목을
찾아 thumbnailUrl이 stub한 "url"과 일치하는지 검증을 추가하세요. 기존 결과 개수 검증은 유지하고, 섹션 1의 대표 이미지 값
자체를 확인하도록 수정하세요.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 4adc8f4e-563e-41d6-9a6d-c580284c2815
📒 Files selected for processing (22)
src/main/java/com/umc/linkyou/converter/CurationConverter.javasrc/main/java/com/umc/linkyou/domain/enums/CurationLinkuType.javasrc/main/java/com/umc/linkyou/domain/mapping/CurationLinku.javasrc/main/java/com/umc/linkyou/repository/UserLinkuRepository/UsersLinkuRepository.javasrc/main/java/com/umc/linkyou/repository/curationRepository/CurationLinkuRepository.javasrc/main/java/com/umc/linkyou/repository/curationRepository/CurationSectionInfoRepository.javasrc/main/java/com/umc/linkyou/service/curation/CurationServiceImpl.javasrc/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.javasrc/main/java/com/umc/linkyou/service/curation/recommend/external/ExternalRecommendReader.javasrc/main/java/com/umc/linkyou/service/curation/recommend/external/ExternalRecommendWorker.javasrc/main/java/com/umc/linkyou/service/curation/recommend/internal/InternalRecommendWorker.javasrc/main/java/com/umc/linkyou/service/curation/utils/ThumbnailUrlProvider.javasrc/main/java/com/umc/linkyou/web/api/CurationApi.javasrc/main/java/com/umc/linkyou/web/controller/CurationController.javasrc/main/java/com/umc/linkyou/web/dto/curation/CurationDetailResponse.javasrc/main/java/com/umc/linkyou/web/dto/curation/CurationLatestResponse.javasrc/main/java/com/umc/linkyou/web/dto/curation/CurationListResponse.javasrc/main/java/com/umc/linkyou/web/dto/curation/CurationSectionResponse.javasrc/main/java/com/umc/linkyou/web/dto/curation/RecommendedLinkResponse.javasrc/main/resources/db/migration/V18__rename_curation_linkus_type_recommended_to_internal.sqlsrc/main/resources/db/migration/V19__drop_curation_linkus_url_normalized.sqlsrc/test/java/com/umc/linkyou/service/curation/CurationServiceTest.java
💤 Files with no reviewable changes (1)
- src/main/java/com/umc/linkyou/service/curation/utils/ThumbnailUrlProvider.java
|
|
||
| private final CurationRepository curationRepository; | ||
| private final CurationLinkuRepository curationLinkuRepository; | ||
| private final UsersLinkuRepository usersLinkuRepository; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
for file in \
src/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.java \
src/main/java/com/umc/linkyou/service/curation/recommend/external/ExternalRecommendWorker.java
do
echo "== $file =="
rg -n -C2 '`@Service`|`@Transactional`' "$file"
doneRepository: LinkYou-2025/LinkU_backend
Length of output: 843
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for file in \
src/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.java \
src/main/java/com/umc/linkyou/service/curation/recommend/external/ExternalRecommendWorker.java
do
echo "== $file =="
wc -l "$file"
sed -n '1,140p' "$file" | cat -n
done
python3 - <<'PY'
from pathlib import Path
files = [
Path("src/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.java"),
Path("src/main/java/com/umc/linkyou/service/curation/recommend/external/ExternalRecommendWorker.java"),
]
for p in files:
text = p.read_text()
lines = text.splitlines()
print(f"== {p} annotation check ==")
for i, line in enumerate(lines, 1):
if "`@Service`" in line or "`@Transactional`" in line:
print(f"{i}: {line.strip()}")
cls_line = next((i for i,l in enumerate(lines,1) if "`@Service`" in l), None)
method_line = next((i for i,l in enumerate(lines,1) if l.startswith(" public void generateExternal") or "public void generateExternal" in l), None)
print(f"class_line={cls_line}, generate_line={method_line}")
PYRepository: LinkYou-2025/LinkU_backend
Length of output: 12968
서비스 기본 읽기 전용 트랜잭션을 적용하세요.
CurationRecommendBuilderServiceImpl은 전용 트랜잭션이 없어 클래스 레벨의 @Transactional(readOnly = true)를 추가하고, ExternalRecommendWorker도 클래스 레벨 기본 읽기 전용 트랜잭션을 적용한 뒤 generateExternal의 저장 매핑에만 메서드 레벨 @Transactional을 유지하세요.
📍 Affects 2 files
src/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.java#L35-L35(this comment)src/main/java/com/umc/linkyou/service/curation/recommend/external/ExternalRecommendWorker.java#L35-L38
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.java`
at line 35, CurationRecommendBuilderServiceImpl 클래스에 `@Transactional`(readOnly =
true)를 적용하세요. ExternalRecommendWorker에도 클래스 레벨 기본 읽기 전용 트랜잭션을 추가하고, 저장 매핑을 수행하는
generateExternal 메서드에는 기존 메서드 레벨 `@Transactional을` 유지하세요. 대상 파일:
src/main/java/com/umc/linkyou/service/curation/recommend/CurationRecommendBuilderServiceImpl.java
35-35,
src/main/java/com/umc/linkyou/service/curation/recommend/external/ExternalRecommendWorker.java
35-38.
Source: Coding guidelines
| @Operation(summary = "가장 최근 큐레이션 조회", description = """ | ||
| 로그인한 사용자의 가장 최근 큐레이션 정보를 조회합니다. | ||
|
|
||
| - 생성된 큐레이션이 하나도 없는 경우, 204 No Content를 반환합니다. | ||
| """) | ||
| @ApiSuccessCode(SuccessStatus._OK) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Locate files =="
fd -a 'CurationApi.java|CurationController.java|ApiResponse|ApiSuccessCode|SuccessStatus' . | sed 's#^\./##' | head -200
echo
echo "== Search relevant symbols =="
rg -n "CurationApi|getLatestCuration|ApiSuccessCode|SuccessStatus|CurationLatestResponse|ApiResponse" src/main/java || trueRepository: LinkYou-2025/LinkU_backend
Length of output: 50382
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== CurationApi.java =="
cat -n src/main/java/com/umc/linkyou/web/api/CurationApi.java | sed -n '1,120p'
echo
echo "== CurationController.java =="
cat -n src/main/java/com/umc/linkyou/web/controller/CurationController.java | sed -n '1,120p'
echo
echo "== CurationService.java =="
cat -n src/main/java/com/umc/linkyou/service/curation/CurationService.java | sed -n '1,80p'
echo
echo "== CurationServiceImpl.java getLatestCuration =="
cat -n src/main/java/com/umc/linkyou/service/curation/CurationServiceImpl.java | sed -n '90,120p'
echo
echo "== ApiResponse.java =="
cat -n src/main/java/com/umc/linkyou/apiPayload/ApiResponse.java | sed -n '1,90p'
echo
echo "== ApiSuccessCode annotation =="
cat -n src/main/java/com/umc/linkyou/validation/annotation/swagger/ApiSuccessCode.java | sed -n '1,80p'
echo
echo "== Swagger config relevant success doc generation =="
cat -n src/main/java/com/umc/linkyou/config/swagger/SwaggerConfig.java | sed -n '1,220p'Repository: LinkYou-2025/LinkU_backend
Length of output: 25251
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== SuccessStatus.java =="
cat -n src/main/java/com/umc/linkyou/apiPayload/code/status/SuccessStatus.java | sed -n '1,80p'
echo
echo "== BaseSuccessCode.java =="
cat -n src/main/java/com/umc/linkyou/apiPayload/code/status/BaseSuccessCode.java | sed -n '1,120p'
echo
echo "== SuccessReasonDTO.java =="
cat -n src/main/java/com/umc/linkyou/apiPayload/code/SuccessReasonDTO.java | sed -n '1,120p'
echo
echo "== Other current NoContent/204 docs/usages =="
rg -n "\.noContent\(\)|204|No Content|apiSuccessCode|ApiSuccessCode" src/main/java --glob '*.java' || true
echo
echo "== Behavioral/static check of getLatestCuration mapping =="
python3 - <<'PY'
from pathlib import Path
import re
controller = Path("src/main/java/com/umc/linkyou/web/controller/CurationController.java").read_text()
m = re.search(r'`@Override`\s+public ResponseEntity<ApiResponse<CurationLatestResponse>> getLatestCuration\([^)]*\)\s*\{(?P<body>.*?)(?=\n `@Override`|\Z)', controller, re.S)
print("controller body:")
print(m.group("body").strip() if m else "NOT FOUND")
print("contains orElse(ResponseEntity.noContent().build()):", "orElse(ResponseEntity.noContent().build())" in (m.group("body") if m else ""))
api = Path("src/main/java/com/umc/linkyou/web/api/CurationApi.java").read_text()
m = re.search(r'`@Operation`\(.*?\)\s+@ApiSuccessCode\(SuccessStatus\._OK\)\s+@GetMapping\("/latest"\)\s+ResponseEntity<ApiResponse<CurationLatestResponse>> getLatestCuration\(', api, re.S)
print("api has `@ApiSuccessCode`(SuccessStatus._OK) before /latest:")
print(bool(m))
PYRepository: LinkYou-2025/LinkU_backend
Length of output: 1538
🏁 Script executed:
#!/bin/bash
set -eo pipefail
echo "== Swagger config continue =="
cat -n src/main/java/com/umc/linkyou/config/swagger/SwaggerConfig.java | sed -n '220,320p'
echo
echo "== Other current NoContent/204 docs/usages/statements =="
rg -n "\.noContent\(\)|204|No Content|apiSuccessCode|ApiSuccessCode" src/main/java --glob '*.java' || true
echo
echo "== Behavioral/static check of getLatestCuration mapping =="
python3 - <<'PY'
from pathlib import Path
import re
controller = Path("src/main/java/com/umc/linkyou/web/controller/CurationController.java").read_text()
m = re.search(r'`@Override`\s+public ResponseEntity<ApiResponse<CurationLatestResponse>> getLatestCuration\([^)]*\)\s*\{(?P<body>.*?)(?=\n `@Override`|\Z)', controller, re.S)
if not m:
print("CONTROLLER_MATCH=NOT_FOUND")
else:
body = m.group("body")
print("CONTROLLER_MATCH=" + ("FOUND" if "orElse(ResponseEntity.noContent().build())" in body else "FOUND_NO_BODY_MATCH"))
print("contains_orElse_noContent_True=" + ("True" if "orElse(ResponseEntity.noContent().build())" in body else "False"))
print("returns_200_for_present_body=" + ("True" if "map(body -> ResponseEntity.ok(ApiResponse.onSuccess(body)))" in body else "False"))
api = Path("src/main/java/com/umc/linkyou/web/api/CurationApi.java").read_text()
api_ok_method = re.search(r'`@ApiSuccessCode`\(SuccessStatus\._OK\)\s+@GetMapping\("/latest"\)\s+ResponseEntity<ApiResponse<CurationLatestResponse>> getLatestCuration\(', api, re.S)
print("api_uses_Projected_StatusCode_for_204=False")
print("api_documented_204_in_description=" + ("True" if bool(api_ok_method) and "204 No Content" in api[api.find(api_ok_method.group(0), 4000) if api_ok_method else len(api):] else "False"))
print("api_has_@ApiSuccessCode_SucccessStatus_OK_before_/latest=" + ("True" if bool(api_ok_method) else "False"))
PYRepository: LinkYou-2025/LinkU_backend
Length of output: 7920
/latest의 204 응답을 Swagger 응답에 반영해 주세요.
CurationController#getLatestCuration은 값이 없을 때 ResponseEntity.noContent().build()를 반환하지만, Swagger 성공 응답은 @ApiSuccessCode(SuccessStatus._OK)로 아직 생성 결과도 포함된 200 응답만 문서화합니다. 204 No Content를 명시적으로 문서화하거나, 빈 결과도 ApiResponse<CurationLatestResponse> 결과로 바꿔 클라이언트 계약과 응답 계약을 맞춰 주세요.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/java/com/umc/linkyou/web/api/CurationApi.java` around lines 52 - 57,
CurationController#getLatestCuration의 빈 결과 계약과 Swagger 문서를 일치시키세요. 값이 없을 때의 204
No Content 응답을 `@ApiSuccessCode`(SuccessStatus._OK)와 함께 명시적으로 문서화하고, 생성된 큐레이션이 있는
경우의 200 응답 및 CurationLatestResponse 스키마는 유지하세요.
Source: Coding guidelines
🔗 관련 이슈
closes #400
📌 작업 내용
1️⃣ Non-Functional Requirement
url_normalized컬럼을 제거했습니다. 정규화된 URL을 별도 컬럼 대신url컬럼에 직접 저장 (V19마이그레이션)CurationConverter로 분리2️⃣ Functional Requirement
RecommendedLinkResponse)에type(INTERNAL/EXTERNAL) 필드 추가userLinkuId를 채워 프론트가 "이미 저장됨"을 판별할 수 있게 함/sections,/history,/latest,/detail,/recommend-links) Swagger 문서 보강🧪 테스트 결과
로컬 동작 확인 완료
📎 참고 사항
외부 추천은
userLinkuId가 기본적으로 null 입니다.추천 받은 이후 사용자가 외부 추천으로 받았던 링크를 저장하면
userLinkuId값이 채워집니다. type은 저장 후에도 외부 추천으로 고정 됩니다.이미 저장했던 링크는 추천 생성에서 제외하고 있습니다.
Summary by CodeRabbit
새로운 기능
개선 사항
문서화